#!/usr/bin/env python2.6

## @package pycodb
#  @brief A simple databasing tool based on directory trees written in Python.
#  @author J Alexander Crough [ http://crough.org ]
#  @version 0.0.5
#  @date 2009

## @mainpage PycoDB Documentation
#
#  @paragraph description
#  PycoDB is a small, lightweight databasing system written in Python. Instead
#  of running a seperate databasing daemon all together, PycoDB uses folder
#  heirarchy to represet tables and rows. This allows for a simple and easy
#  databasing solution great for small projects.
#
#  PycoDB's Homepage can be found at http://pycodb.org
#
#  Copyright 2009, 2010 Jalexander Crough
#
#  PycoDB is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  PycoDB is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.

## @example simple_example.py
#
#  Basic usage example, including selecting tables and retrieving
#  information from rows.

## @example multitable_multirow_example.py
#
#  Usage when more than a single table and row are required by a
#  program.

import os

## Database Class
#
#  PycoDB's main class, to be used to retrieve data from a database.
class Database:
	
	## Constructor
	#
	#  @param self Current instance reference. 
	#  @param dbPath Absolute path to the database folder, expects a string.
	def __init__(self, dbPath):
		## @var dbPath
		#
		#  Absolute path to database folder.
		self.dbPath = dbPath
		## @var currentTable
		#
		#  Currently selected table.
		self.currentTable = None
		## @var tables
		#
		#  List of all tables
		self.tables = self.getTableList()
	
	## Get the current version of PycoDB
	#
	#  @param self Current instance reference. 
	#  @return Current PycoDB version
	def getVersion(self):
		return "0.0.5"
    
	## Get a list of all available tables
	#
	#  @param self Current instance reference. 
	#  @return List of all tables in database
	def getTableList(self):
		tableList = os.listdir(self.dbPath)
		tableList = self.stripHiddenFiles(tableList)
		return tableList
	
	## Set the current working table
	#
	#  @param self Current instance reference. 
	#  @param newTable Table to set currentTable to, defaults to None
	def setTable(self,newTable=None):
		if (newTable is None):
			set.currentTable = None
		else:
			try:
				self.getTableList().index(newTable)
				self.currentTable = newTable
			except ValueError:
				raise TableError("Invalid table selected.")
	
	## Get the path of the currently selected table
	#
	#  @pre currentTable is not None
	#  @param self Current instance reference. 
	#  @return Absolute path to the current table.
	def tablePath(self):
		if self.currentTable is not None:
			return os.path.join(self.dbPath,self.currentTable)	
		else:
			raise TableError("You must select a table before getting its path.")
	
	## Create a new table directory
	#
	#  @pre tableName does not appear in getTableList()
	#  @param self Current instance reference.
    #  @param tableName Name of the new table.
	#  @since 0.0.5
	def createTable(self,tableName):
		if self.getTableList().count(tableName) == 0:
			os.mkdir(os.path.join(self.dbpath,tableName))
		else:
			raise TableError("You cannot create a table which already exists.")
	
	## Remove an existing table
	#
	#  @param self Current instance reference.
	#  @param tableName Name of the table to remove.
	#  @since 0.0.5
	def removeTable(self,tableName):
		os.rmdir(os.path.join(self.dbpath,tableName))
	
	
	## Flush an existing table, i.e. remove all rows from within the table
	#
	#  @param self Current instance reference.
	#  @param tableName Name of the table to flush
	#  @since 0.0.5
	def flushTable(self,tableName):
		oldTable = self.currentTable #Restore the old table after we're done.
		self.setTable(tableName)
		for row in self.getRowList():
			self.deleteRow(row)
		self.setTable(oldTable) #Reset everything
	
		
	## Get a list of all of the rows in the currently selected table
	#
	#  @pre currentTable is not None
	#  @param self Current instance reference. 
	#  @return A list of all rows in the current table.
	def getRowList(self):
		if self.currentTable is not None:
			rows = os.listdir(self.tablePath())
			#Clear hidden files from DB
			rows = self.stripHiddenFiles(rows)
			if len(rows) > 0:
				return rows
			else:
				raise TableError("Table is empty.")
		else:
			raise TableError("You must select a table before listing rows.")
	
	## Get the contents of EVERY row in the currently selected table
	#
	#  @pre currentTable is not None
	#  @param self Current instance reference. 
	#  @return A list of the dictionaries for every row in the current table.
	def getRowContents(self):
		if self.currentTable is not None: #Table must be selected
			rowNames = self.getRowList()
			rowNames = self.stripHiddenFiles(rowNames)
			rowDictList = []
			for row in rowNames: #Get contents of EVERY row
				singleRow = self.getRowById(row)
				rowDictList.append(singleRow)
			return rowDictList
		else:
			raise TableError("A table must be selected in order to continue.")
	
	## Get a row in the currently selected table by its filename
	#
	#  @pre currentTable is not None
	#  @param self Current instance reference. 
	#  @param rowId The filename of the row requested.
	#  @return A dictionary containing the contents of the row
	def getRowById(self,rowId):
		if self.currentTable is not None:
			rowFile = open(os.path.join(self.tablePath(),rowId))
			rowContents = rowFile.read().split("\n\n~")
			rowDict = {}
			rowDict["<ID>"] = rowId
			for entry in rowContents:
				entryPair = entry.split("~\n")
				entryPair[0] = entryPair[0].strip("~")
				entryPair[1] = entryPair[1].rstrip("\n")
				rowDict[entryPair[0]] = entryPair[1]
			rowFile.close()
			return rowDict
		else:
			raise TableError("A table must be selected in order to continue.")
			
	## Remove files starting with a fullstop
	#
	#  @param self Current instance reference. 
	#  @param inputList A list of files, including those starting with a fullstop.
	#  @return inputList with all files starting with a fullstop removed.
	def stripHiddenFiles(self,inputList):
		i = 0
		offset = 0
		newList = inputList
		while i < len(inputList):
			if inputList[i][0] == ".":
				newList.pop(i + offset)
				offset += 1 #We removed one from the inputList, remember?
			i+=1
		return newList
	
	## Write to a row in the currently selected database
	#
	#  @pre currentTable is not None
	#  @param self Current instance reference. 
	#  @param rowId The filename of the row to which data is to be written
	#  @param contents A list of tuples in the form of (key, value) to be written to the row
	#  @since 0.0.4
	def writeToRow(self, rowId, contents):
		if self.currentTable is not None:
			rowFile = open(os.path.join(self.tablePath(),rowId),"w")
			for col in contents:
				rowFile.writelines("~" + col[0] + "~\n" + col[1] + "\n\n");
			rowFile.close()
		else:
			raise TableError("A table must be selected in order to contiune.");
	
	## Delete a row in the database
	#
	#  @pre currentTable is not None
	#  @param self Current instance reference.
	#  @param rowId The filename of the row to be deleted
	#  @since 0.0.4
	def deleteRow(self, rowId):
		if self.currentTable is not None:
			os.remove(os.path.join(self.tablePath(),rowId))
		else:
			raise TableError("A table must be selected in order to continue.")
	
	## Change the contents of a row, can be used to add rows as well
	#
	#  @pre currentTable is not None.
	#  @pre getRowById(rowId) does not throw an IOError
	#  @param self Current instance reference.
	#  @param rowId Id of the row to change data within
	#  @param changeList A list of two-feild tuples, the first feild being the key, the second being the value
	#  @since 0.0.4
	def modifyRowContents(self,rowId,changeList):
		if self.currentTable is not None:
			try:
				rowContentsDict = self.getRowById(rowId)
				del rowContentsDict["<ID>"]
				self.deleteRow(rowId)
				for changeTuple in changeList: #need to loop through keys AND values
					rowContentsDict[changeTuple[0]] = changeTuple[1];
				tupleList = []
				for key,value in rowContentsDict.iteritems(): #need to loop through keys AND values again
					tupleList.append((key,value))
				self.writeToRow(rowId,tupleList)
			except IOError:
				raise RowError("The row " + rowId + " does not exist.") 
		else:
			raise TableError("A table must be selected in order to continue.")
	
	## Change the <ID> (filename) of a row
	#
	#  @pre currentTable is not None.
	#  @pre getRowById(rowId) does not throw an IOError
	#  @param self Current instance reference.
	#  @param rowId Id of the row to change
	#  @since 0.0.5
	def changeRowId(self,rowId):
		if self.currentTable is not None:
			try:
				rowContentsDict = self.getRowById(rowId)
				del rowContentsDict["<ID>"]
				self.deleteRow(rowId)
				tupleList = []
				for key,value in rowContentsDict.iteritems():
					tupleList.append((key,value))
				self.writeToRow(rowId,tupleList)
			except IOError:
				raise RowError("The row " + rowId + " does not exist.")
		else:
			raise TableError("A table must be selected in order to continue.")

## TableError Class
#
#  This is just for custom exceptions to throw. Python's defaults didn't quite fit.
class TableError(Exception):
	## Construcor Method
	#
	# @param self Current instance reference. 
	# @param value The parameter of the exception to throw
	def __init__(self,value):
		## @var parameter
		#
		#  Parameter of the exception to throw.
		self.parameter = value
	## __str__ Method
	#
	#  @param self Current instance reference. 
	#  @return Returns parameter
	def __str__ (self):
		return repr(self.parameter)

## RowError Class
#
#  This is just for custom exceptions to throw. Python's defaults didn't quite fit.
class RowError(Exception):
	## Construcor Method
	#
	# @param self Current instance reference. 
	# @param value The parameter of the exception to throw
	def __init__(self,value):
		## @var parameter
		#
		#  Parameter of the exception to throw.
		self.parameter = value
	## __str__ Method
	#
	#  @param self Current instance reference. 
	#  @return Returns parameter
	def __str__ (self):
		return repr(self.parameter)
		
if __name__ == "__main__":
	raise RuntimeError("This is a module.")